﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using AgFractal.Controls;
using AgFractal.Fractals;
using System.Windows.Threading;

namespace AgFractal
{
    public partial class MainPage : UserControl
    {
#if SILVERLIGHT2
        bool silverlight2 = true;
#else
        bool silverlight2 = false;
#endif
        public MainPage()
        {
            InitializeComponent();
            Init();
            content.FullScreenChanged += (s, e) =>
            {
                AdjustSize();
                CalcFract(false);
            };
            KeyDown += (s, e) =>
            {
                if (e.Key == Key.Ctrl) KeyboardState.CtrlDown = true;
                if (e.Key == Key.Shift) KeyboardState.ShiftDown = true;
            };
            KeyUp += (s, e) =>
            {
                if (e.Key == Key.Ctrl) KeyboardState.CtrlDown = false;
                if (e.Key == Key.Shift) KeyboardState.ShiftDown = false;
            };
            if (silverlight2)
            {
                saveButton.Visibility = Visibility.Collapsed;
                exportButton.Visibility = Visibility.Collapsed;
            }
        }

        List<FractalEntry> fractalList = new List<FractalEntry>();
        private void CalcPreview()
        {
            DateTime st = DateTime.Now;
            foreach (var fractal in FractalManager.Fractals.OrderBy(f => f.Name))
            {
                var fe = new FractalEntry();
                fractalList.Add(fe);
                fe.Fractal = fractal;
                var img = FractalManager.Instance2.CalcFractal(fractal, 210, 140);
                Dispatcher.BeginInvoke(() => fe.Image = img.GetImageSource());
            }
            DateTime et = DateTime.Now;
            var time = et.Subtract(st);
            Dispatcher.BeginInvoke(() => newButton.IsEnabled = true);
        }

        private void Init()
        {
            DataContext = data;
            data.PropertyChanged += (s, e) =>
            {
                if (e.PropertyName == "Fractal") lastEntry = ReadUIValues();
            };
            data.Fractal = FractalManager.Fractals[new Random().Next(FractalManager.Fractals.Count)];
            SetToolMode(ToolMode.Zoom);
            new MouseWheelHelper(this).Moved += MainPage_Moved;
        }

        void MainPage_Moved(object sender, MouseWheelEventArgs e)
        {
            e.Handled = true;
            if (zooming)
                return;
            if (e.Delta > 0)
                ZoomOut(lastMousePosition);
            if (e.Delta < 0)
                ZoomIn(lastMousePosition);            
        }

        private int H;
        private bool once = true;
        private int W;
        private MainPageData data = new MainPageData();
        private Content content = Application.Current.Host.Content;

        private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (content.IsFullScreen) return;
            AdjustSize();
            if (once)
            {
                once = false;
                new Thread(() => CalcPreview()).Start();
                CalcFract(true);
            }
        }

        double sx, sy;
        HistoryEntry lastEntry;
        Stack<HistoryEntry> history = new Stack<HistoryEntry>();
        object calcLock = new object();
        void CalcFract(bool addHistory)
        {
            lock (calcLock)
            {
                imageCanvas.IsHitTestVisible = false;
                Cursor = Cursors.Wait;
                if (addHistory) history.Push(lastEntry.Clone());
                lastEntry = ReadUIValues();
                FractalManager.Instance.MaxIteration = data.Iteration;
                DateTime st = DateTime.Now;
                ThreadPool.QueueUserWorkItem(o =>
                {
                    var img = FractalManager.Instance.CalcFractal(lastEntry, W, H);
                    DateTime et = DateTime.Now;
                    Dispatcher.BeginInvoke(() =>
                    {
                        ShowTime(et.Subtract(st));
                        fractalPresenter.SetImage(img.GetImageSource());
                        Cursor = Cursors.Arrow;
                        zoomRect.Visibility = Visibility.Collapsed;
                        imageCanvas.IsHitTestVisible = true;
                    });
                });
            }
        }

        HistoryEntry ReadUIValues()
        {
            HistoryEntry q = new HistoryEntry(data.Fractal)
            {
                I1 = data.I1,
                I2 = data.I2,
                R1 = data.R1,
                R2 = data.R2
            };

            sx = (data.R2 - data.R1) / (W - 1);
            sy = (data.I2 - data.I1) / (H - 1);

            return q;
        }

        private void AdjustSize()
        {
            var totalW = W = (int)content.ActualWidth;
            var totalH = H = (int)content.ActualHeight;

            W = (W / 9) * 9;
            H = (H / 9) * 9;
            imageCanvas.Width = W;
            imageCanvas.Height = H;
            fractalPresenter.SetSize(W, H);

            LayoutRoot.SetValue(Canvas.LeftProperty, (double)(totalW - W) / 2);
            LayoutRoot.SetValue(Canvas.TopProperty, (double)(totalH - H) / 2);
        }

        private void Calculate_click(object sender, RoutedEventArgs e)
        {
            CalcFract(true);
        }

        bool mouseDown;
        private void imageCanvas_MouseEnter(object sender, MouseEventArgs e)
        {
            if (mouseDown) return;
            HandleZoomMouseEvent(e);
        }

        void ClearZoomData()
        {
            data.ZoomI1 = null;
            data.ZoomI2 = null;
            data.ZoomR1 = null;
            data.ZoomR2 = null;
        }

        Point zoomStartMousePoint;
        Point lastMousePosition;
        void HandleZoomMouseEvent(MouseEventArgs e)
        {
            lastMousePosition = e.GetPosition(imageCanvas);
            if (!mouseDown)
            {
                zoomStartMousePoint = lastMousePosition;
                zoomRect.SetValue(Canvas.TopProperty, zoomStartMousePoint.Y);
                zoomRect.SetValue(Canvas.LeftProperty, zoomStartMousePoint.X);
                data.ZoomR1 = cx(zoomStartMousePoint.X);
                data.ZoomI1 = cy(zoomStartMousePoint.Y);
            }
            else
            {
                var ratio = (double)H / W;
                var zoomWidth = lastMousePosition.X - zoomStartMousePoint.X;
                var zoomHeight = lastMousePosition.Y - zoomStartMousePoint.Y;
                var aspectHeight = zoomWidth * ratio;
                if (aspectHeight > zoomHeight)
                    zoomHeight = aspectHeight;
                else
                    zoomWidth = zoomHeight / ratio;

                if (zoomWidth > 9 && zoomHeight > 9)
                {
                    zoomRect.Width = zoomWidth;
                    zoomRect.Height = zoomHeight;
                    zoomRect.Visibility = Visibility.Visible;
                }
                else
                {
                    zoomRect.Visibility = Visibility.Collapsed;
                    zoomRect.Width = 0;
                    zoomRect.Height = 0;
                }
                data.ZoomR2 = cx(zoomStartMousePoint.X + zoomRect.Width);
                data.ZoomI2 = cy(zoomStartMousePoint.Y + zoomRect.Height);
            }
        }

        private void imageCanvas_MouseLeave(object sender, MouseEventArgs e)
        {
            if (!mouseDown)
                ClearZoomData();
        }

        private void imageCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (toolMode == ToolMode.Zoom)
            {
                HandleZoomMouseEvent(e);
                mouseDown = imageCanvas.CaptureMouse();
            }
        }

        private void imageCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (toolMode == ToolMode.Zoom)
            {
                imageCanvas.ReleaseMouseCapture();
                mouseDown = false;
                if (zoomRect.Visibility == Visibility.Collapsed)
                {
                    data.ZoomI1 = data.ZoomI2 = data.ZoomR1 = data.ZoomR2 = null;
                    var position = e.GetPosition(imageCanvas);
                    if (KeyboardState.CtrlDown)
                        ZoomIn(position);
                    else
                    {
                        ZoomOut(position);
                    }
                }
                else if (data.ZoomR2 != null)
                {
                    data.I1 = data.ZoomI1.Value;
                    data.I2 = data.ZoomI2.Value;
                    data.R1 = data.ZoomR1.Value;
                    data.R2 = data.ZoomR2.Value;
                    data.ZoomI1 = data.ZoomI2 = data.ZoomR1 = data.ZoomR2 = null;

                    var position = e.GetPosition(imageCanvas);
                    zoomRect.Visibility = Visibility.Collapsed;
                    double middleY = zoomStartMousePoint.Y + zoomRect.Height / 2;
                    double middleX = zoomStartMousePoint.X + zoomRect.Width / 2;
                    double factor = imageCanvas.Width / zoomRect.ActualWidth;
                    fractalPresenter.Zoom(new Point(middleX, middleY), factor);
                    CalcFract(true);
                }
            }
        }

        private void ZoomOut(Point position)
        {
            if (KeyboardState.ShiftDown)
            {
                fractalPresenter.Zoom(position, 4);
                Zoom(position, p => FractalManager.Instance.Zoom((int)p.X, (int)p.Y, 4));
            }
            else
            {
                fractalPresenter.Zoom(position, 2);
                Zoom(position, p => FractalManager.Instance.ZoomOut((int)p.X, (int)p.Y));
            }
        }

        private void ZoomIn(Point position)
        {
            fractalPresenter.Zoom(position, 0.5);
            Zoom(position, p => FractalManager.Instance.ZoomIn((int)p.X, (int)p.Y));
        }

        bool zooming;
        private void Zoom(Point position, Func<Point, PngEncoder> render)
        {
            zooming = true;
            imageCanvas.IsHitTestVisible = false;
            Cursor = Cursors.Wait;
            history.Push(lastEntry.Clone());
            FractalManager.Instance.MaxIteration = data.Iteration;
            ThreadPool.QueueUserWorkItem(o =>
            {
                DateTime st = DateTime.Now;
                var img = render(position);
                DateTime et = DateTime.Now;
                Dispatcher.BeginInvoke(() =>
                {
                    ShowTime(et.Subtract(st));
                    fractalPresenter.SetImage(img.GetImageSource());
                    Cursor = Cursors.Arrow;
                    HistoryEntry calcEntry = FractalManager.Instance.LastEntry;
                    data.R1 = calcEntry.R1;
                    data.R2 = calcEntry.R2;
                    data.I1 = calcEntry.I1;
                    data.I2 = calcEntry.I2;
                    lastEntry = ReadUIValues();
                    imageCanvas.IsHitTestVisible = true;
                    zooming = false;
                });
            });
        }

        double cx(double x) { return lastEntry.R1 + sx * x; }
        double cy(double y) { return lastEntry.I1 + sy * y; }

        private void imageCanvas_MouseMove(object sender, MouseEventArgs e)
        {
            HandleZoomMouseEvent(e);
        }

        private void Back_Click(object sender, RoutedEventArgs e)
        {
            if (history.Count > 1)
            {
                var entry = history.Pop();
                data.Fractal = entry.Fractal;
                data.I1 = entry.I1;
                data.I2 = entry.I2;
                data.R1 = entry.R1;
                data.R2 = entry.R2;
                CalcFract(false);
            }
        }

        private void iter_LostFocus(object sender, RoutedEventArgs e)
        {
            short i;
            if (short.TryParse(iter.Text, out i))
                data.Iteration = i;
        }

        ToolMode toolMode;
        private void zooming_Click(object sender, EventArgs e)
        {
            SetToolMode(ToolMode.Zoom);
        }

        private void drag_Click(object sender, EventArgs e)
        {
            SetToolMode(ToolMode.Drag);
        }

        void SetToolMode(ToolMode mode)
        {
            toolMode = mode;
            if (toolMode == ToolMode.Drag)
            {
                fractalPresenter.IsDraggable = true;
                zoom.Opacity = 0.5;
                drag.Opacity = 1;
            }
            else
            {
                fractalPresenter.IsDraggable = false;
                zoom.Opacity = 1;
                drag.Opacity = 0.5;
            }
        }

        private void fractalPresenter_Dragged(object sender, DraggedEventArgs e)
        {
            imageCanvas.IsHitTestVisible = false;
            Cursor = Cursors.Wait;
            fractalPresenter.IsDraggable = false;
            history.Push(lastEntry.Clone());
            FractalManager.Instance.MaxIteration = data.Iteration;
            DateTime st = DateTime.Now;
            var img = FractalManager.Instance.Pan((int)e.HorizontalMove, (int)e.VerticalMove);
            DateTime et = DateTime.Now;
            ShowTime(et.Subtract(st));
            fractalPresenter.SetImage(img.GetImageSource());
            Cursor = Cursors.Arrow;
            fractalPresenter.IsDraggable = true;
            HistoryEntry calcEntry = FractalManager.Instance.LastEntry;
            data.R1 = calcEntry.R1;
            data.R2 = calcEntry.R2;
            data.I1 = calcEntry.I1;
            data.I2 = calcEntry.I2;
            lastEntry = ReadUIValues();
            imageCanvas.IsHitTestVisible = true;
        }

        void ShowTime(TimeSpan ts)
        {
            if (ts.TotalSeconds > 1)
            {
                timeTextBlock.Text = String.Format("Time:{0}:{1}.{2:000}", ts.Minutes, ts.Seconds, ts.Milliseconds);
                showTime.Begin();
            }
        }

        private void fullScreen_Click(object sender, RoutedEventArgs e)
        {
            content.IsFullScreen = !content.IsFullScreen;
        }

        private void Expand()
        {
            opena.Begin();
        }

        private void Collapse()
        {
            closea.Begin();
        }

        private void drag_MouseEnter(object sender, MouseEventArgs e)
        {
            showDrag.Begin();
        }

        private void drag_MouseLeave(object sender, MouseEventArgs e)
        {
            if (toolMode != ToolMode.Drag)
                hideDrag.Begin();
        }

        private void zoom_MouseLeave(object sender, MouseEventArgs e)
        {
            if (toolMode != ToolMode.Zoom)
                hideZoom.Begin();
        }

        private void zoom_MouseEnter(object sender, MouseEventArgs e)
        {
            showZoom.Begin();
        }

        private void Parameter_Click(object sender, RoutedEventArgs e)
        {
            parameterUC.DataContext = data.Fractal.Parameter.FirstOrDefault();
            Show(parameterPopup);
        }

        private void New_click(object sender, RoutedEventArgs e)
        {
            newFractalUC.DataContext = fractalList;
            newFractalUC.OnOk = fe =>
            {
                data.Fractal = fe.Fractal;
                history.Clear();
                CalcFract(true);
            };
            Show(newFractalPopup);
        }

        private void Show(Popup popup)
        {
            IsEnabled = false;
            popup.IsOpen = true;
        }

        private void menuPanel_MouseEnter(object sender, MouseEventArgs e)
        {
            openmenu.Begin();
        }

        private void menuPanel_MouseLeave(object sender, MouseEventArgs e)
        {
            closemenu.Begin();
        }

        private void panel_MouseEnter(object sender, MouseEventArgs e)
        {
            Expand();
        }

        private void panel_MouseLeave(object sender, MouseEventArgs e)
        {
            Collapse();
        }

        private void saveButton_Click(object sender, RoutedEventArgs e)
        {
            Save();
        }

        private void Save()
        {
            var sd = new SavedData
            {
                I1 = lastEntry.I1,
                I2 = lastEntry.I2,
                R1 = lastEntry.R1,
                R2 = lastEntry.R2,
                Fractal = lastEntry.Fractal.ID,
                Params = lastEntry.Fractal.Parameter.ToList(),
                MaxIter = data.Iteration
            };
            SaveUtils.Save(sd);
        }

        private void loadButton_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new OpenFileDialog { Filter = "agfractal files(agf.*)|*.agf", Multiselect = false };

            if (dialog.ShowDialog() != true)
                return;

            using (var stream = dialog.File.OpenRead())
            {
                var ser = new DataContractSerializer(typeof(SavedData));
                var loadedData = ser.ReadObject(stream) as SavedData;

                var fractal = fractalList.FirstOrDefault(f => f.Fractal.ID == loadedData.Fractal);
                if (fractal == null)
                    return;

                data.Fractal = fractal.Fractal;
                foreach (var param in loadedData.Params)
                {
                    var destParam = data.Fractal.Parameter.FirstOrDefault(p => p.Name == param.Name);
                    if (destParam != null)
                    {
                        destParam.I = param.I;
                        destParam.R = param.R;
                    }
                }
                data.I1 = loadedData.I1;
                data.I2 = loadedData.I2;
                data.R1 = loadedData.R1;
                data.R2 = loadedData.R2;
                data.Iteration = (short)loadedData.MaxIter;

                history.Clear();
                CalcFract(true);
            }
        }

        private void exportButton_Click(object sender, RoutedEventArgs e)
        {
            SaveUtils.SaveToJPEG();
        }

        private void gradientMouseUp(object sender, MouseButtonEventArgs e)
        {
            DispatcherTimer timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300)};
            timer.Tick += (s, e2) =>
            {
                timer.Stop();
                FractalManager.Instance.ReApplyColoring();
                fractalPresenter.SetImage(FractalManager.Instance.img.GetImageSource());
            };
            var tmp = FractalManager.Instance.Gradient.Clone();
            FractalManager.Instance.Gradient.GradientChanged += (s, e2) =>
            {
                if (timer.IsEnabled) timer.Stop();
                timer.Start();
            };
            gradientEditor.SetGradient(FractalManager.Instance.Gradient);
            gradientEditor.OnClose = b =>
            {
                if (!b)
                {
                    FractalManager.Instance.Gradient = tmp;
                    ((AgFractal.Coloring.BasicColoring)FractalManager.Instance.coloring).Gradient = tmp;
                    FractalManager.Instance.ReApplyColoring();
                    fractalPresenter.SetImage(FractalManager.Instance.img.GetImageSource());
                }
            };
            Show(gradientEditorPopup);
        }
    }
}